5.16. Структура
Структура
Язык программирования Си предоставляет механизм создания составных типов данных, позволяющих объединять разнородные элементы под единым именем. Этот механизм называется структурой. Структура — это пользовательский тип, состоящий из набора полей, каждое из которых может иметь собственный тип. Благодаря структурам становится возможным моделировать сложные объекты реального мира, такие как человек, автомобиль, книга или банковский счёт, представляя их в виде логически связанных данных.
Объявление и определение структуры
Объявление структуры начинается с ключевого слова struct, за которым следует имя структуры. Это имя становится новым идентификатором типа. После имени в фигурных скобках перечисляются поля — переменные различных типов, которые будут входить в состав структуры. Завершается определение точкой с запятой.
Пример:
struct Book {
char title[100];
char author[50];
int year;
float price;
};
В этом примере создана структура Book, содержащая четыре поля: название книги, имя автора, год издания и цена. Каждое поле имеет свой тип и имя. Память для этих полей выделяется только при создании переменной этого типа, а не при объявлении самой структуры.
Существует также возможность объявить структуру без её определения:
struct Person;
Такое объявление создаёт неполный тип. Компилятор знает, что такой тип существует, но не знает его размера и состава. Неполный тип нельзя использовать для создания переменных, но можно объявлять указатели на него. Это полезно при работе с рекурсивными структурами или при разделении интерфейса и реализации.
Полное определение структуры должно быть доступно в том месте программы, где происходит обращение к её полям или создание экземпляров.
Создание переменных структурного типа
После определения структуры можно объявлять переменные этого типа:
struct Book my_book;
Эта строка резервирует в памяти место, достаточное для хранения всех полей структуры Book. Размер структуры не всегда равен сумме размеров её полей — компилятор может вставлять дополнительные байты между полями для выравнивания по границам памяти, чтобы ускорить доступ к данным. Это поведение зависит от архитектуры процессора и настроек компилятора.
Переменные структурного типа можно инициализировать сразу при объявлении. Существуют два способа инициализации:
-
По позиции: значения указываются в том же порядке, в котором объявлены поля.
struct Book novel = {"1984", "George Orwell", 1949, 29.99}; -
По имени: каждому полю явно присваивается значение, независимо от порядка.
struct Book novel = {.title = "1984", .author = "George Orwell", .year = 1949, .price = 29.99};
Именованная инициализация повышает читаемость кода и снижает вероятность ошибок, особенно когда структура содержит много полей.
Доступ к полям структуры
Для обращения к отдельным полям используется оператор точки (.):
my_book.year = 2025;
printf("Книга: %s, автор: %s\n", my_book.title, my_book.author);
Оператор точки применяется к переменной структурного типа и позволяет читать или изменять значение любого её поля. Все операции над полями выполняются так же, как над обычными переменными соответствующего типа.
Если переменная является указателем на структуру, используется оператор стрелки (->), который совмещает разыменование указателя и доступ к полю:
struct Book *ptr = &my_book;
ptr->price = 34.50;
Это эквивалентно записи (*ptr).price = 34.50;, но более кратко и удобно.
Область видимости структур
Структуры подчиняются тем же правилам области видимости, что и переменные и функции. Если структура объявлена вне функций — на уровне файла, — она доступна во всех функциях, расположенных ниже по тексту программы. Такие структуры обычно размещают в заголовочных файлах (.h), чтобы использовать их в нескольких исходных файлах проекта.
Если структура объявлена внутри функции или даже внутри блока кода (например, внутри фигурных скобок в цикле или условии), она существует только в пределах этой области. Вне блока структура недоступна, и попытка использовать её вызовет ошибку компиляции.
Вложенные объявления структур с одинаковыми именами не приводят к конфликту — внутреннее объявление скрывает внешнее в своей области видимости. Однако переменные, созданные до переопределения, сохраняют свою принадлежность к исходному типу.
Копирование и присваивание структур
Структуры одного типа можно присваивать друг другу напрямую:
struct Book original = {"Dune", "Frank Herbert", 1965, 25.0};
struct Book copy = original;
При таком присваивании выполняется побайтовое копирование содержимого. Все поля получают копии значений из исходной структуры. Это работает корректно для простых типов и массивов фиксированного размера. Однако если структура содержит указатели, копируется только адрес, а не данные, на которые он указывает. Такое поведение называется поверхностным копированием и требует осторожности при работе с динамической памятью.
Упрощение синтаксиса с помощью typedef
Часто используется конструкция typedef для создания псевдонима типа структуры:
typedef struct {
char model[30];
int power;
float weight;
} Car;
Теперь вместо struct Car можно писать просто Car:
Car my_car = {"Tesla Model S", 670, 2100.5};
Это делает код короче и чище, особенно в больших проектах. При использовании typedef имя структуры можно опустить, если оно не требуется для рекурсивных ссылок или других целей.
Альтернативные способы определения
Хотя основной способ — использование struct и, при необходимости, typedef, существуют и другие подходы. Например, препроцессорная директива #define позволяет создать макрос, раскрывающийся в определение структуры:
#define POINT struct { int x; int y; }
POINT p1 = {10, 20};
Такой метод редко используется в современной практике, так как усложняет отладку и снижает читаемость. Он может быть полезен в специфических случаях метапрограммирования или генерации кода, но не рекомендуется для повседневного применения.
Практическое значение структур
Структуры лежат в основе многих концепций системного программирования. Они используются для представления записей в файлах, пакетов в сетевых протоколах, элементов графического интерфейса, узлов деревьев и списков. Без структур невозможно было бы эффективно организовать сложные данные в языке, где отсутствуют встроенные классы или объекты.
Структуры позволяют передавать связанные данные в функции как единый аргумент, а не как множество отдельных параметров. Это упрощает сигнатуры функций и делает код более модульным. Функции могут принимать структуры по значению (копируя их) или по указателю (работая с оригиналом), что даёт контроль над производительностью и семантикой изменений.